//
//  Copyright RevenueCat Inc. All Rights Reserved.
//
//  Licensed under the MIT License (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      https://opensource.org/licenses/MIT
//
//  SystemInfo.swift
//
//  Created by Joshua Liebowitz on 6/29/21.
//

import Foundation

#if os(iOS) || os(tvOS) || VISION_OS || targetEnvironment(macCatalyst)
import UIKit
#elseif os(watchOS)
import UIKit
import WatchKit
#elseif os(macOS)
import AppKit
#endif

// swiftlint:disable file_length
class SystemInfo {

    // swiftlint:disable:next force_unwrapping
    static let appleSubscriptionsURL = URL(string: "https://apps.apple.com/account/subscriptions")!

    static var forceUniversalAppStore: Bool {
        get { self._forceUniversalAppStore.value }
        set { self._forceUniversalAppStore.value = newValue }
    }

    let storeKitVersion: StoreKitVersion
    private let apiKeyValidationResult: Configuration.APIKeyValidationResult

    /// Whether the API key used to configure the SDK is a Test Store API key.
    var isTestStoreAPIKey: Bool {
        return self.apiKeyValidationResult == .testStore
    }

    let operationDispatcher: OperationDispatcher
    let platformFlavor: String
    let platformFlavorVersion: String?
    let responseVerificationMode: Signing.ResponseVerificationMode
    let dangerousSettings: DangerousSettings
    let clock: ClockType
    private let preferredLocalesProvider: PreferredLocalesProvider

    var finishTransactions: Bool {
        get { return self._finishTransactions.value }
        set { self._finishTransactions.value = newValue }
    }

    var isAppBackgroundedState: Bool {
        get { self._isAppBackgroundedState.value }
        set { self._isAppBackgroundedState.value = newValue }
    }

    var bundle: Bundle { return self._bundle.value }

    var observerMode: Bool { return !self.finishTransactions }

    private let sandboxEnvironmentDetector: SandboxEnvironmentDetector
    private let storefrontProvider: StorefrontProviderType
    private let _finishTransactions: Atomic<Bool>
    private let _isAppBackgroundedState: Atomic<Bool>
    private let _bundle: Atomic<Bundle>

    private static let _forceUniversalAppStore: Atomic<Bool> = false
    private static let _proxyURL: Atomic<URL?> = nil

    private lazy var _isSandbox: Bool = {
        return self.sandboxEnvironmentDetector.isSandbox
    }()

    var isSandbox: Bool {
        return self._isSandbox
    }

    var isDebugBuild: Bool {
#if DEBUG
        return true
#else
        return false
#endif
    }

    var storefront: StorefrontType? {
        return self.storefrontProvider.currentStorefront
    }

    static var frameworkVersion: String {
        return "5.36.0-SNAPSHOT"
    }

    static var systemVersion: String {
        return ProcessInfo.processInfo.operatingSystemVersionString
    }

    static var appVersion: String {
        return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
    }

    static var buildVersion: String {
        return Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
    }

    static var bundleIdentifier: String {
        return Bundle.main.bundleIdentifier ?? ""
    }

    static var platformHeader: String {
        return Self.forceUniversalAppStore ? "iOS" : self.platformHeaderConstant
    }

    static var deviceVersion: String {
        var systemInfo = utsname()
        uname(&systemInfo)

        let machineMirror = Mirror(reflecting: systemInfo.machine)
        let identifier = machineMirror.children.reduce("") { identifier, element in
            guard let value = element.value as? Int8, value != 0 else { return identifier }
            return identifier + String(UnicodeScalar(UInt8(value)))
        }

        return identifier
    }

    var identifierForVendor: String? {
        // Should match available platforms in
        // https://developer.apple.com/documentation/uikit/uidevice?language=swift
        // https://developer.apple.com/documentation/watchkit/wkinterfacedevice?language=swift

        #if os(iOS) || os(tvOS) || VISION_OS
            // Fix-me: `UIDevice.current` is `@MainActor` so this method
            // will need to be marked as such too.
            return UIDevice.current.identifierForVendor?.uuidString
        #elseif os(watchOS)
            return WKInterfaceDevice.current().identifierForVendor?.uuidString
        #elseif os(macOS) || targetEnvironment(macCatalyst)
            return self.isSandbox ? MacDevice.identifierForVendor?.uuidString : nil
        #else
            return nil
        #endif
    }

    static var proxyURL: URL? {
        get { return self._proxyURL.value }
        set {
            self._proxyURL.value = newValue

            if let privateProxyURLString = newValue?.absoluteString {
                Logger.info(Strings.configure.configuring_purchases_proxy_url_set(url: privateProxyURLString))
            }
        }
    }

    static let appSessionID = UUID()

    init(platformInfo: Purchases.PlatformInfo?,
         finishTransactions: Bool,
         operationDispatcher: OperationDispatcher = .default,
         bundle: Bundle = .main,
         sandboxEnvironmentDetector: SandboxEnvironmentDetector = BundleSandboxEnvironmentDetector.default,
         storefrontProvider: StorefrontProviderType = DefaultStorefrontProvider(),
         storeKitVersion: StoreKitVersion = .default,
         apiKeyValidationResult: Configuration.APIKeyValidationResult = .validApplePlatform,
         responseVerificationMode: Signing.ResponseVerificationMode = .default,
         dangerousSettings: DangerousSettings? = nil,
         isAppBackgrounded: Bool? = nil,
         clock: ClockType = Clock.default,
         preferredLocalesProvider: PreferredLocalesProvider) {
        self.platformFlavor = platformInfo?.flavor ?? "native"
        self.platformFlavorVersion = platformInfo?.version
        self._bundle = .init(bundle)

        self._finishTransactions = .init(finishTransactions)
        self._isAppBackgroundedState = .init(isAppBackgrounded ?? false)
        self.operationDispatcher = operationDispatcher
        self.storeKitVersion = storeKitVersion
        self.apiKeyValidationResult = apiKeyValidationResult
        self.sandboxEnvironmentDetector = sandboxEnvironmentDetector
        self.storefrontProvider = storefrontProvider
        self.responseVerificationMode = responseVerificationMode
        self.dangerousSettings = dangerousSettings ?? DangerousSettings()
        self.clock = clock
        self.preferredLocalesProvider = preferredLocalesProvider

        if isAppBackgrounded == nil {
            self.isApplicationBackgrounded { isAppBackgrounded in
                self.isAppBackgroundedState = isAppBackgrounded
            }
        }
    }

    var supportsOfflineEntitlements: Bool {
        !self.observerMode && !self.dangerousSettings.customEntitlementComputation
    }

    /// Asynchronous API to check if app is backgrounded at a specific moment.
    func isApplicationBackgrounded(completion: @escaping @Sendable (Bool) -> Void) {
        self.operationDispatcher.dispatchOnMainActor {
            var isApplicationBackgrounded: Bool = false
            #if os(iOS) || os(tvOS) || VISION_OS
            isApplicationBackgrounded = self.isApplicationBackgroundedIOSAndTVOS
            #elseif os(watchOS)
            isApplicationBackgrounded = self.isApplicationBackgroundedWatchOS
            #endif
            completion(isApplicationBackgrounded)
        }
    }

    #if targetEnvironment(simulator)
    static let isRunningInSimulator = true
    #else
    static let isRunningInSimulator = false
    #endif

    func isOperatingSystemAtLeast(_ version: OperatingSystemVersion) -> Bool {
        return ProcessInfo.processInfo.isOperatingSystemAtLeast(version)
    }

    /// Checks for exposure to https://github.com/RevenueCat/purchases-ios/issues/4954
    func isSubjectToKnownIssue_18_4_sim() -> Bool {
        let firstOSVersionWithBug = OperatingSystemVersion(majorVersion: 18,
                                                           minorVersion: 4,
                                                           patchVersion: 0)

        // Conservative estimate. No Simulator iOS fix version currently known (as at 2025-04-15).
        let firstOSVersionWithFix = OperatingSystemVersion(majorVersion: 18,
                                                           minorVersion: 5,
                                                           patchVersion: 0)

        return SystemInfo.isRunningInSimulator
            && self.isOperatingSystemAtLeast(firstOSVersionWithBug)
            && !self.isOperatingSystemAtLeast(firstOSVersionWithFix)
    }

    #if os(iOS) || os(tvOS) || VISION_OS
    var sharedUIApplication: UIApplication? {
        return Self.sharedUIApplication
    }

    static var sharedUIApplication: UIApplication? {
        return UIApplication.value(forKey: "sharedApplication") as? UIApplication
    }

    #endif

    static func isAppleSubscription(managementURL: URL) -> Bool {
        guard let host = managementURL.host else { return false }
        return host.contains("apple.com")
    }

    /// Returns the preferred locales, including the locale override if set.
    var preferredLocales: [String] {
        return self.preferredLocalesProvider.preferredLocales
    }

    /// Developer-set preferred locale.
    ///
    /// `preferredLocales` already includes it if set, so this property is only useful for reading the override value.
    var preferredLocaleOverride: String? {
        return self.preferredLocalesProvider.preferredLocaleOverride
    }

    func overridePreferredLocale(_ locale: String?) {
        self.preferredLocalesProvider.overridePreferredLocale(locale)
    }
}

#if os(iOS) || os(tvOS) || VISION_OS
extension SystemInfo {

    @available(iOS 13.0, macCatalyst 13.1, tvOS 13.0, *)
    @available(macOS, unavailable)
    @available(watchOS, unavailable)
    @available(watchOSApplicationExtension, unavailable)
    @MainActor
    var currentWindowScene: UIWindowScene {
        get throws {
            let scene = self.sharedUIApplication?.currentWindowScene

            return try scene.orThrow(ErrorUtils.storeProblemError(withMessage: "Failed to get UIWindowScene"))
        }
    }
}
#endif

extension SystemInfo: SandboxEnvironmentDetector {}

// @unchecked because:
// - Class is not `final` (it's mocked). This implicitly makes subclasses `Sendable` even if they're not thread-safe.
extension SystemInfo: @unchecked Sendable {}

extension SystemInfo {

    #if targetEnvironment(macCatalyst)
    static let platformHeaderConstant = "uikitformac"
    #elseif os(iOS)
    static let platformHeaderConstant = "iOS"
    #elseif os(watchOS)
    static let platformHeaderConstant = "watchOS"
    #elseif os(tvOS)
    static let platformHeaderConstant = "tvOS"
    #elseif os(macOS)
    static let platformHeaderConstant = "macOS"
    #elseif VISION_OS
    static let platformHeaderConstant = "visionOS"
    #endif

    static var applicationWillEnterForegroundNotification: Notification.Name {
        #if os(iOS) || os(tvOS) || VISION_OS
            UIApplication.willEnterForegroundNotification
        #elseif os(macOS)
            NSApplication.willBecomeActiveNotification
        #elseif os(watchOS)
            Notification.Name.NSExtensionHostWillEnterForeground
        #endif
    }

    static var applicationDidEnterBackgroundNotification: Notification.Name {
        #if os(iOS) || os(tvOS) || VISION_OS
            UIApplication.didEnterBackgroundNotification
        #elseif os(macOS)
            NSApplication.didResignActiveNotification
        #elseif os(watchOS)
            Notification.Name.NSExtensionHostDidEnterBackground
        #endif
    }

    /// Returns the appropriate `Notification.Name` for the OS's didBecomeActive notification,
    /// indicating that the application did become active. This value is only nil for watchOS
    /// versions below 7.0.
    static var applicationDidBecomeActiveNotification: Notification.Name? {
        #if os(iOS) || os(tvOS) || VISION_OS || targetEnvironment(macCatalyst)
            return UIApplication.didBecomeActiveNotification
        #elseif os(macOS)
            return NSApplication.didBecomeActiveNotification
        #elseif os(watchOS)
        if #available(watchOS 9, *) {
            return WKApplication.didBecomeActiveNotification
        } else if #available(watchOS 7, *) {
            // Work around for "Symbol not found" dyld crashes on watchOS 7.0..<9.0
            return Notification.Name("WKApplicationDidBecomeActiveNotification")
        } else {
            // There's no equivalent notification available on watchOS <7.
            return nil
        }
        #endif
    }

    var isAppExtension: Bool {
        return self.bundle.bundlePath.hasSuffix(".appex")
    }
}

private extension SystemInfo {

    #if os(iOS) || os(tvOS) || VISION_OS

    // iOS/tvOS App extensions can't access UIApplication.sharedApplication, and will fail to compile if any calls to
    // it are made. There are no pre-processor macros available to check if the code is running in an app extension,
    // so we check if we're running in an app extension at runtime, and if not, we use KVC to call sharedApplication.
    @MainActor
    var isApplicationBackgroundedIOSAndTVOS: Bool {
        if self.isAppExtension {
            return true
        }

        guard let sharedUIApplication = self.sharedUIApplication else { return false }
        return sharedUIApplication.applicationState == .background
    }

    #elseif os(watchOS)

    @MainActor
    var isApplicationBackgroundedWatchOS: Bool {
        var isSingleTargetApplication: Bool {
            return Bundle.main.infoDictionary?.keys.contains("WKApplication") == true
        }

        if #available(watchOS 7.0, *), self.isOperatingSystemAtLeast(.init(majorVersion: 9,
                                                                           minorVersion: 0,
                                                                           patchVersion: 0)) {
            // `WKApplication` works on both dual-target and single-target apps
            // When running on watchOS 9.0+
            return WKApplication.shared().applicationState == .background
        } else {
            if isSingleTargetApplication {
                // Before watchOS 9.0, single-target apps don't allow using `WKExtension` or `WKApplication`
                // (see https://github.com/RevenueCat/purchases-ios/issues/1891)
                // So we can't detect if it's running in the background
                return false
            } else {
                return WKExtension.shared().applicationState == .background
            }
        }
    }

    #endif
}
